using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace ScreenToGif.Controls;

/// <summary>
/// The Resizing Adorner controls. https://social.msdn.microsoft.com/Forums/vstudio/en-US/274bc547-dadf-42b5-b3f1-6d29407f9e79/resize-adorner-scale-problem?forum=wpf
/// </summary>
public class ResizingAdorner : Adorner
{
    #region Variables

    /// <summary>
    /// Resizing adorner uses Thumbs for visual elements.  
    /// The Thumbs have built-in mouse input handling.
    /// </summary>
    private readonly Thumb _topLeft, _topRight, _bottomLeft, _bottomRight, _middleBottom, _middleTop, _leftMiddle, _rightMiddle;

    /// <summary>
    /// The dashed border.
    /// </summary>
    private Rectangle _rectangle;

    /// <summary>
    /// To store and manage the adorner's visual children.
    /// </summary>
    readonly VisualCollection _visualChildren;

    /// <summary>
    /// The current adorned element.
    /// </summary>
    private UIElement _adornedElement;

    /// <summary>
    /// The parent of the element.
    /// </summary>
    private readonly UIElement _parent;

    /// <summary>
    /// The latest position of the element. Used by the drag operation.
    /// </summary>
    private Point _lastestPosition;

    #endregion

    /// <summary>
    /// Initialize the ResizingAdorner.
    /// </summary>
    /// <param name="adornedElement">The element to be adorned.</param>
    /// <param name="isMovable">True if it's available the drag to move action.</param>
    /// <param name="parent">The parent of the element.</param>
    /// <param name="startPosition">The start position of the first click.</param>
    public ResizingAdorner(UIElement adornedElement, bool isMovable = true, UIElement parent = null, Point? startPosition = null)
        : base(adornedElement)
    {
        _visualChildren = new VisualCollection(this);

        #region Default values

        _adornedElement = adornedElement;
        _parent = parent ?? (_adornedElement as FrameworkElement)?.Parent as UIElement;

        if (startPosition.HasValue)
            _lastestPosition = startPosition.Value;

        #endregion

        //Creates the dashed rectangle around the adorned element.
        BuildAdornerBorder();

        //Call a helper method to initialize the Thumbs with a customized cursors.
        BuildAdornerCorner(ref _topLeft, Cursors.SizeNWSE);
        BuildAdornerCorner(ref _topRight, Cursors.SizeNESW);
        BuildAdornerCorner(ref _bottomLeft, Cursors.SizeNESW);
        BuildAdornerCorner(ref _bottomRight, Cursors.SizeNWSE);

        BuildAdornerCorner(ref _middleBottom, Cursors.SizeNS);
        BuildAdornerCorner(ref _middleTop, Cursors.SizeNS);
        BuildAdornerCorner(ref _leftMiddle, Cursors.SizeWE);
        BuildAdornerCorner(ref _rightMiddle, Cursors.SizeWE);

        //Drag to move.
        if (isMovable)
        {
            _adornedElement.PreviewMouseLeftButtonDown += AdornedElement_PreviewMouseLeftButtonDown;
            _adornedElement.MouseMove += AdornedElement_MouseMove;
            _adornedElement.MouseUp += AdornedElement_MouseUp;
        }

        //Add handlers for resizing • Corners
        _bottomLeft.DragDelta += HandleBottomLeft;
        _bottomRight.DragDelta += HandleBottomRight;
        _topLeft.DragDelta += HandleTopLeft;
        _topRight.DragDelta += HandleTopRight;

        //Add handlers for resizing • Sides
        _middleBottom.DragDelta += HandleBottomMiddle;
        _middleTop.DragDelta += HandleTopMiddle;
        _leftMiddle.DragDelta += HandleLeftMiddle;
        _rightMiddle.DragDelta += HandleRightMiddle;
    }

    private void AdornedElement_MouseUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
    {
        _adornedElement?.ReleaseMouseCapture();
    }

    private void AdornedElement_MouseMove(object sender, MouseEventArgs e)
    {
        if (_parent == null)
            return;

        if (_adornedElement is Image && e.LeftButton == MouseButtonState.Pressed)
        {
            _adornedElement.MouseMove -= AdornedElement_MouseMove;

            var currentPosition = e.GetPosition(_parent);

            Canvas.SetLeft(_adornedElement, Canvas.GetLeft(_adornedElement) + (currentPosition.X - _lastestPosition.X));
            Canvas.SetTop(_adornedElement, Canvas.GetTop(_adornedElement) + (currentPosition.Y - _lastestPosition.Y));

            _lastestPosition = currentPosition;

            _adornedElement.MouseMove += AdornedElement_MouseMove;
        }
    }

    private void AdornedElement_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        if (_parent == null)
            return;

        if (_adornedElement is Image && _adornedElement.CaptureMouse())
            _lastestPosition = e.GetPosition(_parent);
    }

    #region DragDelta Event Handlers

    /// <summary>
    /// Handler for resizing from the bottom-right.
    /// </summary>
    private void HandleBottomRight(object sender, DragDeltaEventArgs args)
    {
        if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb)
            return;

        var zoomFactor = GetCanvasZoom(AdornedElement);

        // Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        // Change the size by the amount the user drags the mouse, as long as it's larger 
        // than the width or height of an adorner, respectively.
        adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width);
        adornedElement.Height = Math.Max(adornedElement.Height + args.VerticalChange / zoomFactor, hitThumb.DesiredSize.Height);

        //Adjust canvas size.
        if (adornedElement.Parent is FrameworkElement canvas)
        {
            //Right.
            var elementLeft = Canvas.GetLeft(adornedElement);
            var elementRight = elementLeft + adornedElement.Width;

            if (elementRight > canvas.Width)
                canvas.Width = elementRight;

            //Bottom.
            var elementTop = Canvas.GetTop(adornedElement);
            var elementBottom = elementTop + adornedElement.Height;

            if (elementBottom > canvas.Height)
                canvas.Height = elementBottom;
        }
    }

    /// <summary>
    ///  Handler for resizing from the top-right.
    /// </summary>
    private void HandleTopRight(object sender, DragDeltaEventArgs args)
    {
        if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb)
            return;

        // Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        var zoomFactor = GetCanvasZoom(AdornedElement);

        //Change the size by the amount the user drags the mouse, as long as it's larger than the width or height of an adorner, respectively.
        adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width);
        
        var heightOld = adornedElement.Height;
        var heightNew = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
        var topOld = Canvas.GetTop(adornedElement);
        adornedElement.Height = heightNew;

        Canvas.SetTop(adornedElement, topOld - (heightNew - heightOld));

        //Adjust Canvas Right.
        var elementLeft = Canvas.GetLeft(adornedElement);
        var elementRight = elementLeft + adornedElement.Width;

        if (adornedElement.Parent is FrameworkElement canvas && elementRight > canvas.Width)
            canvas.Width = elementRight;
    }

    /// <summary>
    ///  Handler for resizing from the top-left.
    /// </summary>
    private void HandleTopLeft(object sender, DragDeltaEventArgs args)
    {
        if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb)
            return;

        //Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        var zoomFactor = GetCanvasZoom(AdornedElement);

        //Change the size by the amount the user drags the mouse, as long as it's larger than the width or height of an adorner, respectively.
        var widthOld = adornedElement.Width;
        var widthNew = Math.Max(adornedElement.Width - args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width);
        var leftOld = Canvas.GetLeft(adornedElement);

        adornedElement.Width = widthNew;
        Canvas.SetLeft(adornedElement, leftOld - (widthNew - widthOld));

        var heightOld = adornedElement.Height;
        var heightNew = Math.Max(adornedElement.Height - args.VerticalChange / zoomFactor, hitThumb.DesiredSize.Height);
        var topOld = Canvas.GetTop(adornedElement);

        adornedElement.Height = heightNew;
        Canvas.SetTop(adornedElement, topOld - (heightNew - heightOld));
    }

    /// <summary>
    ///  Handler for resizing from the bottom-left.
    /// </summary>
    private void HandleBottomLeft(object sender, DragDeltaEventArgs args)
    {
        if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb)
            return;

        //Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        var zoomFactor = GetCanvasZoom(AdornedElement);

        //Change the size by the amount the user drags the mouse, as long as it's larger than the width or height of an adorner, respectively.
        adornedElement.Height = Math.Max(adornedElement.Height + args.VerticalChange /zoomFactor, hitThumb.DesiredSize.Height);

        var widthOld = adornedElement.Width;
        var widthNew = Math.Max(adornedElement.Width - args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width);
        var leftOld = Canvas.GetLeft(adornedElement);

        adornedElement.Width = widthNew;
        Canvas.SetLeft(adornedElement, leftOld - (widthNew - widthOld));

        //Adjust Canvas Bottom.
        var elementTop = Canvas.GetTop(adornedElement);
        var elementBottom = elementTop + adornedElement.Height;

        if (adornedElement.Parent is FrameworkElement canvas && elementBottom > canvas.Height)
            canvas.Height = elementBottom;
    }

    /// <summary>
    /// Handler for resizing from the bottom-middle.
    /// </summary>
    private void HandleBottomMiddle(object sender, DragDeltaEventArgs args)
    {
        if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb)
            return;
        
        // Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        var zoomFactor = GetCanvasZoom(AdornedElement);
        
        //Change the size by the amount the user drags the mouse, as long as it's larger than the height of an adorner.
        adornedElement.Height = Math.Max(adornedElement.Height + args.VerticalChange / zoomFactor, hitThumb.DesiredSize.Height);

        //Adjust Canvas Bottom.
        var elementTop = Canvas.GetTop(adornedElement);
        var elementBottom = elementTop + adornedElement.Height;

        if (adornedElement.Parent is FrameworkElement canvas && elementBottom > canvas.Height)
            canvas.Height = elementBottom;
    }

    /// <summary>
    /// Handler for resizing from the top-middle.
    /// </summary>
    private void HandleTopMiddle(object sender, DragDeltaEventArgs args)
    {
        if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb)
            return;

        // Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        var zoomFactor = GetCanvasZoom(AdornedElement);

        // Change the size by the amount the user drags the mouse, as long as it's larger than the height of an adorner.
        var heightOld = adornedElement.Height;
        var heightNew = Math.Max(adornedElement.Height - args.VerticalChange / zoomFactor, hitThumb.DesiredSize.Height);
        var topOld = Canvas.GetTop(adornedElement);

        adornedElement.Height = heightNew;
        Canvas.SetTop(adornedElement, topOld - (heightNew - heightOld));
    }

    /// <summary>
    /// Handler for resizing from the left-middle.
    /// </summary>
    private void HandleLeftMiddle(object sender, DragDeltaEventArgs args)
    {
        if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb)
            return;

        // Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        var zoomFactor = GetCanvasZoom(AdornedElement);

        // Change the size by the amount the user drags the mouse, as long as it's larger than the height of an adorner.
        var widthOld = adornedElement.Width;
        var widthNew = Math.Max(adornedElement.Width - args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width);
        var leftOld = Canvas.GetLeft(adornedElement);

        adornedElement.Width = widthNew;
        Canvas.SetLeft(adornedElement, leftOld - (widthNew - widthOld));
    }

    /// <summary>
    ///  Handler for resizing from the right-middle.
    /// </summary>
    private void HandleRightMiddle(object sender, DragDeltaEventArgs args)
    {
        if (AdornedElement is not FrameworkElement adornedElement || sender is not Thumb hitThumb)
            return;
        
        //Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        var zoomFactor = GetCanvasZoom(AdornedElement);

        //Change the size by the amount the user drags the mouse, as long as it's larger than the width of the adorner.
        adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange / zoomFactor, hitThumb.DesiredSize.Width);

        //Adjust Canvas Right.
        var elementLeft = Canvas.GetLeft(adornedElement);
        var elementRight = elementLeft + adornedElement.Width;

        if (adornedElement.Parent is FrameworkElement canvas && elementRight > canvas.Width)
            canvas.Width = elementRight;
    }

    #endregion

    #region Private Methods

    /// <summary>
    ///  Arrange the Adorners.
    /// </summary>
    /// <param name="finalSize">The final Size</param>
    /// <returns>The final size</returns>
    protected override Size ArrangeOverride(Size finalSize)
    {
        //Width and height of the element that's being adorned. 
        var desiredWidth = AdornedElement.DesiredSize.Width;
        var desiredHeight = AdornedElement.DesiredSize.Height;

        _topLeft.Arrange(new Rect(-desiredWidth / 2, -desiredHeight / 2, desiredWidth, desiredHeight));
        _topRight.Arrange(new Rect(desiredWidth / 2, -desiredHeight / 2, desiredWidth, desiredHeight));
        _bottomLeft.Arrange(new Rect(-desiredWidth / 2, desiredHeight / 2, desiredWidth, desiredHeight));
        _bottomRight.Arrange(new Rect(desiredWidth / 2, desiredHeight / 2, desiredWidth, desiredHeight));

        _middleBottom.Arrange(new Rect(0, desiredHeight / 2, desiredWidth, desiredHeight));
        _middleTop.Arrange(new Rect(0, -desiredHeight / 2, desiredWidth, desiredHeight));
        _leftMiddle.Arrange(new Rect(-desiredWidth / 2, 0, desiredWidth, desiredHeight));
        _rightMiddle.Arrange(new Rect(desiredWidth / 2, 0, desiredWidth, desiredHeight));

        var zoomFactor = GetCanvasZoom(AdornedElement);

        _rectangle.Arrange(new Rect(0, 0, desiredWidth * zoomFactor, desiredHeight * zoomFactor));

        return finalSize;
    }

    /// <summary>
    /// Instantiates the corner Thumbs, setting the Cursor property, 
    /// some appearance properties, and add the elements to the visual tree.
    /// </summary>
    /// <param name="cornerThumb">The Thumb to Instantiate.</param>
    /// <param name="customizedCursor">The custom cursor.</param>
    private void BuildAdornerCorner(ref Thumb cornerThumb, Cursor customizedCursor)
    {
        if (cornerThumb != null)
            return;

        cornerThumb = new Thumb { Cursor = customizedCursor };
        cornerThumb.Height = cornerThumb.Width = 10;
        cornerThumb.Style = (Style)FindResource("ScrollBar.Thumb");

        _visualChildren.Add(cornerThumb);
    }

    /// <summary>
    /// Creates the dashed border around the adorned element.
    /// </summary>
    private void BuildAdornerBorder()
    {
        _rectangle = new Rectangle();
        _rectangle.StrokeDashArray.Add(5);
        _rectangle.Stroke = new SolidColorBrush(Color.FromRgb(171, 171, 171));
        _rectangle.StrokeThickness = 1;

        _visualChildren.Add(_rectangle);
    }

    // This method ensures that the Widths and Heights are initialized.  Sizing to content produces
    // Width and Height values of Double.NaN.  Because this Adorner explicitly resizes, the Width and Height
    // need to be set first.  It also sets the maximum size of the adorned element.
    private void EnforceSize(FrameworkElement adornedElement)
    {
        if (adornedElement.Width.Equals(Double.NaN))
            adornedElement.Width = adornedElement.DesiredSize.Width;

        if (adornedElement.Height.Equals(Double.NaN))
            adornedElement.Height = adornedElement.DesiredSize.Height;

        //if (adornedElement.Parent is FrameworkElement parent)
        //{
        //    adornedElement.MaxHeight = parent.ActualHeight;
        //    adornedElement.MaxWidth = parent.ActualWidth;
        //}
    }

    // Override the VisualChildrenCount and GetVisualChild properties to interface with 
    // the adorner's visual collection.
    protected override int VisualChildrenCount => _visualChildren.Count;

    /// <summary>
    /// Gets the VisualChildren at given position.
    /// </summary>
    /// <param name="index">The Index to look for.</param>
    /// <returns>The VisualChildren</returns>
    protected override Visual GetVisualChild(int index)
    {
        return _visualChildren[index];
    }

    #endregion

    private double GetCanvasZoom(Visual referenceVisual)
    {
        if (referenceVisual is Canvas canvas1)
            return canvas1.LayoutTransform.Value.M11;

        var parent = VisualTreeHelper.GetParent(referenceVisual) as Visual;

        if (parent is Canvas canvas2)
            return canvas2.LayoutTransform.Value.M11;

        return 1;
    }

    public override GeneralTransform GetDesiredTransform(GeneralTransform transform)
    {
        var zoomFactor = GetCanvasZoom(AdornedElement);

        _topLeft.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor);
        _topRight.RenderTransformOrigin = new Point(0.5, 0.5);
        _topRight.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor);
        _topRight.RenderTransformOrigin = new Point(0.5, 0.5);
        _bottomLeft.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor);
        _bottomLeft.RenderTransformOrigin = new Point(0.5, 0.5);
        _bottomRight.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor);
        _bottomRight.RenderTransformOrigin = new Point(0.5, 0.5);

        _middleBottom.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor);
        _middleBottom.RenderTransformOrigin = new Point(0.5, 0.5);
        _middleTop.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor);
        _middleTop.RenderTransformOrigin = new Point(0.5, 0.5);
        _rightMiddle.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor);
        _rightMiddle.RenderTransformOrigin = new Point(0.5, 0.5);
        _leftMiddle.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor);
        _leftMiddle.RenderTransformOrigin = new Point(0.5, 0.5);
        _rectangle.RenderTransform = new ScaleTransform(1 / zoomFactor, 1 / zoomFactor);
        //_rectangle.RenderTransformOrigin = new Point(0.5, 0.5);

        return base.GetDesiredTransform(transform);
    }

    public void Destroy()
    {
        _adornedElement.PreviewMouseLeftButtonDown -= AdornedElement_PreviewMouseLeftButtonDown;
        _adornedElement.MouseMove -= AdornedElement_MouseMove;
        _adornedElement.MouseUp -= AdornedElement_MouseUp;

        _adornedElement = null;
    }
}